namespace ScrumSprintMonitor.ServerAdapter.TFS
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Isolation.TeamFoundationServer;

	#endregion

	public class TfsBuildInfo : IBuildInfo,
	                            IDisposable
	{
		#region Private Fields

		private readonly ITfsBuildServer buildServer;
		private readonly Func<ITfsBuildDetail[]> fetchBuildDetailsDelegate;
		private readonly ILogger<TfsBuildInfo> logger;
		private readonly ISprintConfiguration sprintConfiguration;
		private readonly SynchronizationContext syncContext;
		private readonly Timer timer;
		private bool disposed;
		private Exception exception;
		private IAsyncResult fetchBuildDetailsAsyncResult;
		private DateTime finishTime;
		private string number;
		private string requestedFor;
		private BuildStatus status;

		#endregion

		public TfsBuildInfo(ISprintConfiguration sprintConfiguration, ITfsBuildServer buildServer, ILogger<TfsBuildInfo> logger)
		{
			this.sprintConfiguration = sprintConfiguration;
			this.buildServer = buildServer;
			this.logger = logger;

			this.syncContext = SynchronizationContext.Current;

			this.fetchBuildDetailsDelegate = new Func<ITfsBuildDetail[]>(FetchBuildDetails);

			if (this.syncContext != null)
			{
				this.timer = new Timer(OnTimer, null, 1000, Timeout.Infinite);
			}
		}

		#region Implementation of IBuildInfo

		public string Number
		{
			get { return this.number; }
			private set
			{
				if (value != this.number)
				{
					this.number = value;
					OnPropertyChanged("Number");
				}
			}
		}

		public DateTime FinishTime
		{
			get { return this.finishTime; }
			private set
			{
				if (value != this.finishTime)
				{
					this.finishTime = value;
					OnPropertyChanged("FinishTime");
				}
			}
		}

		public BuildStatus Status
		{
			get { return this.status; }
			private set
			{
				if (value != this.status)
				{
					this.status = value;
					OnPropertyChanged("Status");
				}
			}
		}

		public Exception Exception
		{
			get { return this.exception; }
			private set
			{
				if (value != this.exception)
				{
					this.exception = value;

					this.logger.Info(value, "Exception assigned to IBuildInfo.Exception.");

					OnPropertyChanged("Exception");
				}
			}
		}

		public string RequestedFor
		{
			get { return this.requestedFor; }
			set
			{
				if (value != this.requestedFor)
				{
					this.requestedFor = value;

					OnPropertyChanged("RequestedFor");
				}
			}
		}

		#endregion

		private void OnTimer(object state)
		{
			if (this.disposed)
			{
				return;
			}
			if (string.IsNullOrEmpty(this.sprintConfiguration.ProjectName) || this.sprintConfiguration.BuildDefinitionNames == null ||
			    this.sprintConfiguration.BuildDefinitionNames.Length == 0)
			{
				Status = BuildStatus.NotSetUp;
				return;
			}

			Debug.Assert(this.fetchBuildDetailsAsyncResult == null || this.fetchBuildDetailsAsyncResult.IsCompleted);

			this.fetchBuildDetailsAsyncResult =
				this.fetchBuildDetailsDelegate.BeginInvoke(fetchState => this.syncContext.Post(OnBuildDetailsAvailable, fetchState), null);
		}

		private ITfsBuildDetail[] FetchBuildDetails()
		{
			var buildDetails =
				this.sprintConfiguration.BuildDefinitionNames.AsParallel().AsUnordered().Where(x => !string.IsNullOrEmpty(x)).Select(
					x => this.buildServer.GetBuildDetail(this.sprintConfiguration.ProjectName, x)).Where(x => x != null).ToArray();

			return buildDetails;
		}

		private void OnBuildDetailsAvailable(object state)
		{
			Debug.Assert(!Thread.CurrentThread.IsBackground);

			try
			{
				this.fetchBuildDetailsAsyncResult = null;

				var buildDetails = (IEnumerable<ITfsBuildDetail>) this.fetchBuildDetailsDelegate.EndInvoke((IAsyncResult) state);

				if (buildDetails.Any())
				{
					var buildDetail = SelectBuildDetail(buildDetails);

					Status = TranslateBuildStatus(buildDetail.Status);
					Number = buildDetail.BuildNumber;
					FinishTime = buildDetail.FinishTime;
					RequestedFor = buildDetail.RequestedFor;
				}
				else
				{
					Status = BuildStatus.NotStarted;
					Number = string.Empty;
					FinishTime = DateTime.MinValue;
					RequestedFor = string.Empty;
				}

				Exception = null; // Clear exception
			}
			catch (AggregateException ae)
			{
				this.logger.Error(ae, "Exception detected while fetching latest build status.");

				Exception = ae.InnerExceptions[0];
			}
			catch (TeamFoundationServerException tfse)
			{
				this.logger.Error(tfse, "Exception detected while fetching latest build status.");

				Exception = tfse;
			}
			finally
			{
				RescheduleQuery();
			}
		}

		private void RescheduleQuery()
		{
			if (this.timer != null && !this.disposed)
			{
				this.timer.Change(10000, Timeout.Infinite);
			}
		}

		private static ITfsBuildDetail SelectBuildDetail(IEnumerable<ITfsBuildDetail> buildDetails)
		{
			var lowestBuildStatus = buildDetails.Min(x => x.Status);

			if (lowestBuildStatus == TfsBuildStatus.Succeeded)
			{
				return buildDetails.OrderBy(x => x.LastChangedOn).Last();
			}

			return buildDetails.OrderBy(x => x.LastChangedOn).First(x => x.Status == lowestBuildStatus);
		}

		private static BuildStatus TranslateBuildStatus(TfsBuildStatus buildStatus)
		{
			switch (buildStatus)
			{
				case TfsBuildStatus.InProgress:
					return BuildStatus.InProgress;
				case TfsBuildStatus.Succeeded:
					return BuildStatus.Succeeded;
				case TfsBuildStatus.PartiallySucceeded:
					return BuildStatus.PartiallySucceeded;
				case TfsBuildStatus.Failed:
					return BuildStatus.Failed;
				case TfsBuildStatus.Stopped:
					return BuildStatus.Stopped;
				case TfsBuildStatus.NotStarted:
					return BuildStatus.NotStarted;
				default:
					throw new ArgumentOutOfRangeException("buildStatus");
			}
		}

		#region Implementation of INotifyPropertyChanged

		public event PropertyChangedEventHandler PropertyChanged;

		private void OnPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		#endregion

		#region Implementation of IDisposable

		/// <summary>
		///                     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		/// <filterpriority>2</filterpriority>
		public void Dispose()
		{
			this.disposed = true;

			if (this.timer != null)
			{
				this.timer.Change(Timeout.Infinite, Timeout.Infinite);
				this.timer.Dispose();
			}
		}

		#endregion
	}
}